昨天晚上回顾了以前在onenote上记的动态规划笔记,发现很多程序都有相似之处,且最近两天写的动态规划程序都没有一遍AC。所以将这两天写的动态规划程序总结至此,以便背诵、默写用(这种题被罚时实在太亏)。
背诵的时候要特别注意dp数组的功能和其递推公式。
阅读指南
六道题分别是钱币兑换、0-1背包、完全背包、最长公共子序列、最长上升子序列、划分数。
前两题较简单,从第三题开始有公式推导,二和三题末尾有技巧总结。
一、hdu1284 钱币兑换问题
先来道最简单的背诵。
【题目描述】
一个国家只有1,2,3分钱,输入非负整数n(不超过10000),输出兑换金额n一共有多少种换法,多组输入输出。
【示例程序】
(没有一遍AC的原因写在了注释里)
1 |
|
二、0-1背包问题
【题目描述】
第一行输入n代表共有n(不超过100)种物品,第二行依次输入这些物品的重量(不超过100),第三行依次输入这些物品的价值(不超过100),第四行输入背包能承受的总重(10000),输出背包能装的物品的最大总价值。
比如输入:1
2
3
44
3 1 2 3
4 2 3 2
5
输出
1 | 7 |
【示例代码】
1 |
|
【总结】
可以看出来,这种类型的动态规划的核心是初始化并完善dp数组,大致顺序就是:
0、察觉到这是动态规划题,确定大致算法流程;
1、确认dp[i][j]含义和递推式;
2、初始化dp数组;
3、完善dp数组。
然后就是利用dp数组中的元素回答问题。
这道题犯的错集中在dp数组的完善部分,说明我需要注意数组下标变化、注意递推式的正确使用,以及最终的的是:保持对dp数组功能的认知
不能一遍AC的根源
在被这两道题疯狂罚时之后,我发现我的错误都不是算法问题,而是集中在数组下标没把握好上,属于细节问题。于是博主决定不轻视任何一道题,任何题都要在纸上写出算法思路、数据结构,规定好数据范围、数组下标这类细节,然后再进行编码。抱着这样的想法,我做了一道0-1背包升级版——完全背包问题,这一次,终于一遍就AC了:
三、完全背包问题
【题目描述】
依旧是输入物品种数n,每种物品的重量,每个物品的价值,背包的承重上限,输出背包能装的物品的最大总价值。和0-1背包问题不同的是,每种物品能选无限多件。
【示例代码】
1 |
|
展示以下我草稿纸上定义的dp数组的功能和递推关系的推导:
dp[i][j]代表从前i类(0~i-1号)物品挑选出总重不超过j的最大价值。
dp数组的初始化:显然dp[0][…]应当都为0。
递推关系推导:
这个表达式可以简化,大括号中除了第一项,其余项的最大值就是$dp[i][j-w[i-1]]+v[i-1]$,
所以,递推关系可以简化成如下:
总之遇到动态规划的题,遵循以下步骤可以大大降低错误率
1、在纸上书写大致流程、数据(存储)结构;
2、规定dp数组的含义;
3、初始化dp数组;
4、确定递推式完善dp数组;
5、根据确定的dp数组回答问题。
⚠️注意不要轻视任何题目,以及有条件的话背诵一些经典的动态规划代码,比如本文写的几个。
以上所提放在其他类型的算法题上,也是适用的。
四、最长公共子序列问题
和背包问题思路不同的动态规划题。
【问题描述】分别输入字符串s和t的长度,再输入s和t两个字符串,输出s和t的最长公共子序列
比如输入:
1 | 4 4 |
由于两个字符串的公共部分是bcd,有三个字符,则输出:
1 | 3 |
由于在上一题已经尝到了先在纸上分析的甜头,所以这一题先进行分析:
1、规定dp数组:dp[i][j]代表s[1]~s[i]和t[1]~t[j]的公共子序列,注意我不用s[0]和t[0],所以定义存储串s和串t的数组的长度应当额外加一;
2、初始化dp数组:dp[0][…]和dp[…][0]肯定都为0;
3、确定递推关系:
如果$s[i+1]==t[i+1]$,则
反之
4、程序Output:dp[n][m],n和m分别为用户输入的s和t的长度。
【示例代码】
我又一次因为纸上打草稿而避免了罚时
1 |
|
五、最长上升子序列
【题目描述】
图片来自《挑战程序设计竞赛(第2版)》
这道题和上道题题目很像,但意思完全不同。依旧先分析实现方法:
1、dp[i+1]代表以a[i]结尾的最长上升子序列的长度;
2、初始化dp[0]和dp[1]为0;
3、递推公式是:
$dp[i]=max{1,dp[j]+1|j<i且a[j]<a[i]}$
【示例代码】
1 |
|
六、有关计数问题的dp——划分数
【题目描述】
将题目中“模M的余数”去掉,我们直接输出划分方法总数。
图片来自《挑战程序设计竞赛(第2版)》。
实现方法分析:
1、dp[i][j]代表j划分为不超过i组的种数;
2、初始化dp[0][非零]为0,因为没有非零数能被划分为不超过0组,dp[…][0]为1,0被划分为任意多组方式都为一种;
3、j划分成不超过i组,可以等价为将j划分为i组,然后每一组的值可以为0。这样的话就假设j划分成的i个数都为非零和至少一个零两种情况,前者等于dp[i][j-i],后者等于dp[i-1][j],所以递推式如下:
$dp[i][j]=dp[i][j-i]+dp[i-1][j]$
$(i>=1,j>=1)$
⚠️注意不要将dp[i][j-i]写成dp[i][j-1]
【示例代码】
1 |
|
小结
把以上几道题背会,足以掌握动态规划的基本方法,也足以举一反三地应对简单一些的赛事和考试。对于高级赛事,仍需要多练习,感悟为主。